package com.huangkailong.designpattern.prototype;

import com.esotericsoftware.kryo.Kryo;
import com.esotericsoftware.kryo.io.Input;
import com.esotericsoftware.kryo.io.Output;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.lang.reflect.Field;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;

/**
 * 深克隆工具类型.
 *
 * @author huangkl
 * @since 1.0.0
 */
public final class DeepCloneUtil {
    private static final Kryo KRYO = new Kryo();
    private static final Set<Class<?>> REGISTER_CLASSES = new HashSet<>();

    static {
        KRYO.setRegistrationRequired(false);
    }


    /**
     * 深克隆对象
     *
     * @param obj 克隆对象
     * @param <T> 克隆对象类型
     * @return 成功克隆的对象
     */
    public static <T> T clone(T obj) {
        Class<T> type = (Class<T>) obj.getClass();
        registerClassIfNeed(obj);
        return doClone(obj, type);
    }

    private static <T> T doClone(T obj, Class<T> type) {
        try (
            ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
            Output output = new Output(outputStream)
        ) {
            KRYO.writeObject(output, obj);
            output.flush();
            try (
                ByteArrayInputStream inputStream = new ByteArrayInputStream(
                    outputStream.toByteArray());
                Input input = new Input(inputStream)) {
                return KRYO.readObject(input, type);
            }
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException(e.getMessage(), e);
        }
    }

    private static void registerClassIfNeed(Object obj) {
        if (Objects.isNull(obj)) {
            return;
        }
        Class<?> clazz = obj.getClass();
        registerClassIfNeed(obj, clazz);
    }

    private static void registerClassIfNeed(Object obj, Class<?> clazz) {
        if (!REGISTER_CLASSES.contains(clazz)) {
            synchronized (DeepCloneUtil.class) {
                if (!REGISTER_CLASSES.contains(clazz)) {
                    KRYO.register(clazz);
                    REGISTER_CLASSES.add(clazz);
                    Field[] declaredFields = clazz.getDeclaredFields();
                    for (Field declaredField : declaredFields) {
                        declaredField.setAccessible(true);
                        try {
                            Object value = declaredField.get(obj);
                            registerClassIfNeed(value);
                            if (Objects.nonNull(value) && value.getClass() != declaredField.getType()) {
                                registerClassIfNeed(declaredField.getType());
                            }
                        } catch (IllegalAccessException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        }
    }
}
